/**
 * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information regarding copyright ownership. Apereo
 * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use
 * this file except in compliance with the License. You may obtain a copy of the License at the
 * following location:
 *
 * <p>http://www.apache.org/licenses/LICENSE-2.0
 *
 * <p>Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apereo.portal.rendering;

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import org.apereo.portal.utils.cache.CacheKey;
import org.apereo.portal.xml.XmlUtilitiesImpl;
import org.apereo.portal.xml.stream.FilteringXMLEventReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanNameAware;

/** */
public class StAXAttributeIncorporationComponent extends StAXPipelineComponentWrapper
        implements BeanNameAware {
    protected final Logger logger = LoggerFactory.getLogger(getClass());

    private final XMLEventFactory eventFactory = XMLEventFactory.newFactory();

    private AttributeSource attributeSource;

    private String beanName;

    public void setAttributeSource(AttributeSource attributeSource) {
        this.attributeSource = attributeSource;
    }

    @Override
    public void setBeanName(String name) {
        this.beanName = name;
    }

    /* (non-Javadoc)
     * @see org.apereo.portal.rendering.PipelineComponent#getCacheKey(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
     */
    @Override
    public CacheKey getCacheKey(HttpServletRequest request, HttpServletResponse response) {
        final CacheKey parentKey = this.wrappedComponent.getCacheKey(request, response);
        final CacheKey attributeKey = this.attributeSource.getCacheKey(request, response);
        return CacheKey.build(this.beanName, parentKey, attributeKey);
    }

    /* (non-Javadoc)
     * @see org.apereo.portal.rendering.PipelineComponent#getEventReader(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
     */
    @Override
    public PipelineEventReader<XMLEventReader, XMLEvent> getEventReader(
            HttpServletRequest request, HttpServletResponse response) {
        // Get the reader from the parent and add the attribute incorporating wrapper
        final PipelineEventReader<XMLEventReader, XMLEvent> cachingEventReader =
                this.wrappedComponent.getEventReader(request, response);
        final XMLEventReader eventReader =
                new AttributeIncorporatingXMLEventReader(
                        request, response, cachingEventReader.getEventReader());

        final Map<String, String> outputProperties = cachingEventReader.getOutputProperties();
        return new PipelineEventReaderImpl<XMLEventReader, XMLEvent>(eventReader, outputProperties);
    }

    private final class AttributeIncorporatingXMLEventReader extends FilteringXMLEventReader {
        private final HttpServletRequest request;
        private final HttpServletResponse response;

        public AttributeIncorporatingXMLEventReader(
                HttpServletRequest request, HttpServletResponse response, XMLEventReader reader) {
            super(reader);
            this.request = request;
            this.response = response;
        }

        @Override
        protected XMLEvent filterEvent(XMLEvent event, boolean peek) {
            // Only filtering start elements to add attributes
            if (!event.isStartElement()) {
                return event;
            }

            // Get the additional attributes, return if there are no attributes for this element
            final StartElement startElement = event.asStartElement();
            final Iterator<Attribute> additionalAttributes =
                    attributeSource.getAdditionalAttributes(request, response, startElement);
            if (additionalAttributes == null || !additionalAttributes.hasNext()) {
                return event;
            }

            // Merge the additional attributes with the existing attributes
            // additional attributes overwrite
            final Map<QName, Attribute> mergedAttributes = new LinkedHashMap<QName, Attribute>();

            for (final Iterator<Attribute> attributes = startElement.getAttributes();
                    attributes.hasNext(); ) {
                final Attribute attribute = attributes.next();
                mergedAttributes.put(attribute.getName(), attribute);
            }

            while (additionalAttributes.hasNext()) {
                final Attribute attribute = additionalAttributes.next();
                mergedAttributes.put(attribute.getName(), attribute);

                if (logger.isDebugEnabled()) {
                    logger.debug(
                            "{} - Adding Attribute {} to event {}",
                            new Object[] {
                                beanName,
                                XmlUtilitiesImpl.toString(attribute),
                                XmlUtilitiesImpl.toString(event)
                            });
                }
            }

            // Create the modified StartElement with the additional attribute data
            final StartElement modifiedStartElement =
                    eventFactory.createStartElement(
                            startElement.getName(),
                            mergedAttributes.values().iterator(),
                            startElement.getNamespaces());

            return modifiedStartElement;
        }
    }
}
